a tool for shared writing and social publishing
at debug/datetime 245 lines 8.8 kB view raw
1"use client"; 2import { AtUri } from "@atproto/syntax"; 3import { PubLeafletDocument } from "lexicons/api"; 4import { EditTiny } from "components/Icons/EditTiny"; 5 6import { usePublicationData } from "./PublicationSWRProvider"; 7import { Fragment, useState } from "react"; 8import { useParams } from "next/navigation"; 9import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 10import { Menu, MenuItem } from "components/Layout"; 11import { deletePost } from "./deletePost"; 12import { ButtonPrimary } from "components/Buttons"; 13import { MoreOptionsVerticalTiny } from "components/Icons/MoreOptionsVerticalTiny"; 14import { DeleteSmall } from "components/Icons/DeleteSmall"; 15import { ShareSmall } from "components/Icons/ShareSmall"; 16import { ShareButton } from "components/ShareOptions"; 17import { SpeedyLink } from "components/SpeedyLink"; 18import { QuoteTiny } from "components/Icons/QuoteTiny"; 19import { CommentTiny } from "components/Icons/CommentTiny"; 20import { useLocalizedDate } from "src/hooks/useLocalizedDate"; 21 22export function PublishedPostsList(props: { 23 searchValue: string; 24 showPageBackground: boolean; 25}) { 26 let { data } = usePublicationData(); 27 let params = useParams(); 28 let { publication } = data!; 29 if (!publication) return null; 30 if (publication.documents_in_publications.length === 0) 31 return ( 32 <div className="italic text-tertiary w-full container text-center place-items-center flex flex-col gap-3 p-3"> 33 Nothing's been published yet... 34 </div> 35 ); 36 return ( 37 <div className="publishedList w-full flex flex-col gap-2 pb-4"> 38 {publication.documents_in_publications 39 .sort((a, b) => { 40 let aRecord = a.documents?.data! as PubLeafletDocument.Record; 41 let bRecord = b.documents?.data! as PubLeafletDocument.Record; 42 const aDate = aRecord.publishedAt 43 ? new Date(aRecord.publishedAt) 44 : new Date(0); 45 const bDate = bRecord.publishedAt 46 ? new Date(bRecord.publishedAt) 47 : new Date(0); 48 return bDate.getTime() - aDate.getTime(); // Sort by most recent first 49 }) 50 .map((doc) => { 51 if (!doc.documents) return null; 52 let leaflet = publication.leaflets_in_publications.find( 53 (l) => doc.documents && l.doc === doc.documents.uri, 54 ); 55 let uri = new AtUri(doc.documents.uri); 56 let record = doc.documents.data as PubLeafletDocument.Record; 57 let quotes = doc.documents.document_mentions_in_bsky[0]?.count || 0; 58 let comments = doc.documents.comments_on_documents[0]?.count || 0; 59 60 return ( 61 <Fragment key={doc.documents?.uri}> 62 <div className="flex gap-2 w-full "> 63 <div 64 className={`publishedPost grow flex flex-col hover:no-underline! rounded-lg border ${props.showPageBackground ? "border-border-light py-1 px-2" : "border-transparent px-1"}`} 65 style={{ 66 backgroundColor: props.showPageBackground 67 ? "rgba(var(--bg-page), var(--bg-page-alpha))" 68 : "transparent", 69 }} 70 > 71 <div className="flex justify-between gap-2"> 72 <a 73 className="hover:no-underline!" 74 target="_blank" 75 href={`${getPublicationURL(publication)}/${uri.rkey}`} 76 > 77 <h3 className="text-primary grow leading-snug"> 78 {record.title} 79 </h3> 80 </a> 81 <div className="flex justify-start align-top flex-row gap-1"> 82 {leaflet && ( 83 <SpeedyLink 84 className="pt-[6px]" 85 href={`/${leaflet.leaflet}`} 86 > 87 <EditTiny /> 88 </SpeedyLink> 89 )} 90 <Options document_uri={doc.documents.uri} /> 91 </div> 92 </div> 93 94 {record.description ? ( 95 <p className="italic text-secondary"> 96 {record.description} 97 </p> 98 ) : null} 99 <div className="text-sm text-tertiary flex gap-1 flex-wrap pt-3"> 100 {record.publishedAt ? ( 101 <PublishedDate dateString={record.publishedAt} /> 102 ) : null} 103 {(comments > 0 || quotes > 0) && record.publishedAt 104 ? " | " 105 : ""} 106 {quotes > 0 && ( 107 <SpeedyLink 108 href={`${getPublicationURL(publication)}/${uri.rkey}?interactionDrawer=quotes`} 109 className="flex flex-row gap-1 text-sm text-tertiary items-center" 110 > 111 <QuoteTiny /> {quotes} 112 </SpeedyLink> 113 )} 114 {comments > 0 && quotes > 0 ? " " : ""} 115 {comments > 0 && ( 116 <SpeedyLink 117 href={`${getPublicationURL(publication)}/${uri.rkey}?interactionDrawer=comments`} 118 className="flex flex-row gap-1 text-sm text-tertiary items-center" 119 > 120 <CommentTiny /> {comments} 121 </SpeedyLink> 122 )} 123 </div> 124 </div> 125 </div> 126 {!props.showPageBackground && ( 127 <hr className="last:hidden border-border-light" /> 128 )} 129 </Fragment> 130 ); 131 })} 132 </div> 133 ); 134} 135 136let Options = (props: { document_uri: string }) => { 137 return ( 138 <Menu 139 align="end" 140 alignOffset={20} 141 asChild 142 trigger={ 143 <button className="text-secondary rounded-md selected-outline border-transparent! hover:border-border! h-min"> 144 <MoreOptionsVerticalTiny /> 145 </button> 146 } 147 > 148 <> 149 <OptionsMenu document_uri={props.document_uri} /> 150 </> 151 </Menu> 152 ); 153}; 154 155function OptionsMenu(props: { document_uri: string }) { 156 let { mutate, data } = usePublicationData(); 157 let [state, setState] = useState<"normal" | "confirm">("normal"); 158 159 let postLink = data?.publication 160 ? `${getPublicationURL(data?.publication)}/${new AtUri(props.document_uri).rkey}` 161 : null; 162 163 if (state === "normal") { 164 return ( 165 <> 166 <ShareButton 167 className="justify-end" 168 text={ 169 <div className="flex gap-2"> 170 Share Post Link 171 <ShareSmall /> 172 </div> 173 } 174 subtext="" 175 smokerText="Post link copied!" 176 id="get-post-link" 177 fullLink={postLink?.includes("https") ? postLink : undefined} 178 link={postLink} 179 /> 180 181 <hr className="border-border-light" /> 182 <MenuItem 183 className="justify-end" 184 onSelect={async (e) => { 185 e.preventDefault(); 186 setState("confirm"); 187 return; 188 }} 189 > 190 Delete Post 191 <DeleteSmall /> 192 </MenuItem> 193 </> 194 ); 195 } 196 if (state === "confirm") { 197 return ( 198 <div className="flex flex-col items-center font-bold text-secondary px-2 py-1"> 199 Are you sure? 200 <div className="text-sm text-tertiary font-normal"> 201 This action cannot be undone! 202 </div> 203 <ButtonPrimary 204 className="mt-2" 205 onClick={async () => { 206 await mutate((data) => { 207 if (!data) return data; 208 return { 209 ...data, 210 publication: { 211 ...data.publication!, 212 leaflets_in_publications: 213 data.publication?.leaflets_in_publications.filter( 214 (l) => l.doc !== props.document_uri, 215 ) || [], 216 documents_in_publications: 217 data.publication?.documents_in_publications.filter( 218 (d) => d.documents?.uri !== props.document_uri, 219 ) || [], 220 }, 221 }; 222 }, false); 223 await deletePost(props.document_uri); 224 }} 225 > 226 Delete 227 </ButtonPrimary> 228 </div> 229 ); 230 } 231} 232 233function PublishedDate(props: { dateString: string }) { 234 const formattedDate = useLocalizedDate(props.dateString, { 235 year: "numeric", 236 month: "long", 237 day: "2-digit", 238 }); 239 240 return ( 241 <p className="text-sm text-tertiary"> 242 Published {formattedDate} 243 </p> 244 ); 245}